﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.Threading;
using System.Runtime.InteropServices;
using System.IO;

namespace PS3Helpers.Helpers
{
    public class ProcessExecutor : IDisposable
    {
        private StringBuilder _standardOutput = new StringBuilder();
        private StringBuilder _standardError = new StringBuilder();
        private int _exitCode;

        public ProcessExecutor(OutputLogInterface loginterface, string workingdirectory, string filename, string arguments, bool readoutput)
            : this(loginterface, workingdirectory, filename, arguments, 5, readoutput)
        {
        }

        public ProcessExecutor(OutputLogInterface loginterface, string workingdirectory, string filename, string arguments, int maximumminutes, bool readoutput)
        {
            if (string.IsNullOrEmpty(workingdirectory))
            {
                workingdirectory = Path.GetDirectoryName(filename).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
            }

            
            _standardOutput.Clear();

            // loginterface.SetCurrentStatus(string.Format("Running '{0}'", filename));

            string origdir = Environment.CurrentDirectory;
            try
            {
                Environment.CurrentDirectory = workingdirectory;

                using (Process process = new Process())
                {
                    // filename = GetExecutablePath(filename);

                    process.StartInfo.FileName = filename;
                    process.StartInfo.Arguments = arguments;
                    process.StartInfo.WorkingDirectory = workingdirectory;

                    // Is it a console- or is it a Win32 application?
                    // Win32 applications (aka GUI-programs) will not produce any 
                    // StdOut or StdErr, so don't try to redirect those streams.
                    if ((readoutput) && (IsConsoleType(GetExecutablePath(filename))))
                    {
                        process.StartInfo.UseShellExecute = false;
                        process.StartInfo.RedirectStandardOutput = true;
                        process.StartInfo.RedirectStandardError = true;
                    }
                    else
                    {
                        process.StartInfo.UseShellExecute = true;
                    }

                    process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
                    process.StartInfo.CreateNoWindow = true;
                    process.StartInfo.ErrorDialog = false;


                    process.Start();

                    // The stream readers won't do anything if no redirection has been set
                    using (StreamReaderThread stdoutreader = new StreamReaderThread(process, ProcessStreamReadType.StandardOutput, _standardOutput))
                    {
                        using (StreamReaderThread stderrreader = new StreamReaderThread(process, ProcessStreamReadType.StandardError, _standardError))
                        {
                            if (!process.WaitForExit(maximumminutes * 60 * 1000))
                            {
                                process.Kill();
                            }

                            _exitCode = process.ExitCode;
                            
                            if (_exitCode != 0)
                            {
                                if (_standardError.Length > 0)
                                {
                                    loginterface.WriteDebug(_standardError.ToString());
                                }

                                if (_standardOutput.Length > 0)
                                {
                                    loginterface.WriteDebug(_standardOutput.ToString());
                                }
                            }
                        }
                    }


                    if (_exitCode != 0)
                    {
                        loginterface.SetCurrentStatus("Process exited with exitcode: " + _exitCode.ToString());
                    }
                }
            }
            finally
            {
                Environment.CurrentDirectory = origdir;
            }
        }

        #region Dispose Implementation

        ~ProcessExecutor()
        {
            Dispose(false);
        }

        bool disposed = false;
        protected virtual void Dispose(bool disposing)
        {
            if (!disposed)
            {
                if (disposing)
                {
                    // Dispose managed resources.
                    _standardOutput = null;
                    _standardError = null;
                }

                // There are no unmanaged resources to release, but
                // if we add them, they need to be released here.
            }

            disposed = true;

            // If it is available, make the call to the
            // base class's Dispose(Boolean) method
            // base.Dispose(disposing);
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        #endregion Dispose Implementation

        public string StandardOutput
        {
            get { return _standardOutput.ToString(); }
        }

        public string StandardError
        {
            get { return _standardError.ToString(); }
        }

        public int ExitCode
        {
            get { return _exitCode; }
        }

        #region Process Console Information

        /*
         * GetExecutablePath()
         * Locate the full path of the executable specified e.g. "find" or "find.exe".
         */
        private static string GetExecutablePath(string executable)
        {
            if (Path.GetDirectoryName(executable) != "")
                return executable;

            List<string> filenames = new List<string>();
            if (Path.GetExtension(executable) == "")
            {
                filenames.Add(executable + ".exe");
                filenames.Add(executable + ".com");
                filenames.Add(executable + ".cmd");
                filenames.Add(executable + ".bat");
            }
            else
            {
                filenames.Add(executable);
            }

            string pathstr = Environment.GetEnvironmentVariable("Path");
            foreach (string path in pathstr.Split(';'))
            {
                string curpath = path.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;

                foreach (string exename in filenames)
                {
                    if (File.Exists(curpath + exename))
                    {
                        return curpath + exename;
                    }
                }
            }

            return executable;
        }

        private static bool IsConsoleType(string nativeExeModulePath)
        {
            SHFILEINFO fi = new SHFILEINFO();
            int res = SHGetFileInfo(nativeExeModulePath, 0, ref fi, SHFILEINFO.MAX_PATH, SHFILEINFO.SHGFI_EXETYPE);
            return res != 0 && (res & 0xFFFF) == SHFILEINFO.PE_SIGN && ((res >> 16) & 0xFFFF) == 0;
        }

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
        private struct SHFILEINFO
        {
            private const int NAMESIZE = 80;
            internal const int PE_SIGN = 0x4550;
            internal const int MAX_PATH = 260;
            internal const int SHGFI_EXETYPE = 0x2000;
            public IntPtr hIcon;
            public int iIcon;
            public int dwAttributes;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = MAX_PATH)]
            public string szDisplayName;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = NAMESIZE)]
            public string szTypeName;
        }

        [DllImport("shell32")]
        private static extern int SHGetFileInfo(string pszPath, uint dwFileAttributes, ref SHFILEINFO psfi, uint cbFileInfo, uint uFlags);

        #endregion Process Console Information
    }
}
